
/**
 * nobittercontrol.cpp
 * 
 * NobitterControl controls the entire application
 * 
 * Copyright (c) 2008 Harunobu Oyama <nobukichi at gmail.com>
 * Licensed under GNU GPL version 3
 *
 */

#include <QTimer>
#include <QApplication>
#include <QMessageBox>
#include <QFileInfo>
#include <QDir>
#include <qDebug>
#include "concretefilters.h"
#include "usersettings.h"
#include "twitterapi.h"
#include "nobittercipher.h"
#include "nobittermodel.h"
#include "nobittercontrol.h"
#include "settingsdialog.h"
#include "imagecache.h"

const char* const DEFAULT_BASE_URL = "http://code.google.com/p/nobitter/?dmy=";
const char* const NOBITTER_HEADER = "encrypted by nobitter ";

inline QString logFilePath(const QString& userName)
{
	return UserSettings::get()->logPath() + "/nobitter-" + userName + ".log";
}


NobitterControl::NobitterControl(QObject* parent)
  : QObject(parent)
{
	updateProxy();

	authenticationDone = false;
	pCipher = new NobitterCipher(DEFAULT_BASE_URL, UserSettings::get()->encryptionKey(), this);
	pModel = new NobitterModel(this);
	pTwitter = new TwitterAPI(this);
	pUpdateTimer = new QTimer(this);

	setupWhenFilter();
	setupAutoUpdateTimer();
	connectNobitterCipherSignals();
	connectTwitterApiSignals();
}

void NobitterControl::slotStart()
{
	if ( UserSettings::get()->twitterUserName().isEmpty() ||
		UserSettings::get()->twitterPassword().isEmpty() ) {
		QTimer::singleShot(0, this, SLOT(slotShowSettingsDialog()));
	} else {
		slotSetUserName(UserSettings::get()->twitterUserName());
		slotSetGroupGatewayName(UserSettings::get()->gatewayUserName());
		pTwitter->setTwitterCredentials(userName, UserSettings::get()->twitterPassword());
	}
}

void NobitterControl::setupAutoUpdateTimer()
{
	//! setup auto-update timer, but not start it yet
	pUpdateTimer->setSingleShot(false);
	pUpdateTimer->setInterval(UserSettings::get()->autoUpdateInterval() * 60 * 1000);
	connect( pUpdateTimer, SIGNAL(timeout()), 
	         this, SLOT(slotUpdateMyTimeline()) );
	pUpdateTimer->start();
}

void NobitterControl::connectNobitterCipherSignals()
{
	connect( pCipher, SIGNAL(signalEncrypted(const QString&, const QString&)),
	         this, SLOT(slotSendEncryptedTweet(const QString&, const QString&)) );
	connect( pCipher, SIGNAL(signalDecrypted(const QString&, const QString&)),
	         this, SLOT(slotFeedDecryptedTweet(const QString&, const QString&)) );
	connect( pCipher, SIGNAL(signalDetectedHttpError(QHttp::Error)),
	         this, SLOT(slotReportTinyUrlError(QHttp::Error)) );
}


void NobitterControl::connectTwitterApiSignals()
{
	connect( pTwitter, SIGNAL(signalReceivedTweets(int, const TweetVector&)),
	         this, SLOT(slotFeedTweets(int, const TweetVector&)) );
	connect( pTwitter, SIGNAL(signalReceivedUserList(int, const QStringList&)),
	         this, SLOT(slotSetGroupMemberList(int, const QStringList&)) );
	connect( pTwitter, SIGNAL(signalDetectedHttpError(QHttp::Error)),
	         this, SLOT(slotReportTwitterError(QHttp::Error)) );
	connect( pTwitter, SIGNAL(signalSetCredentialsDone()),
	         this, SLOT(slotUpdateMyTimeline()) );
	connect( pTwitter, SIGNAL(signalAuthenticationFailure(int)),
	         this, SLOT(slotAlertAuthenticationFailure(int)) );
	connect( pTwitter, SIGNAL(signalRequestStarted()),
	         this, SLOT(slotEmitSignalRequestStarted()) );
	connect( pTwitter, SIGNAL(signalRequestDone()),
	         this, SLOT(slotEmitSignalRequestDone()) );
	connect( pTwitter, SIGNAL(signalGetFollowingUsersOfFailed(int)),
	         this, SLOT(slotShowNoSuchGroupError(int)) );
}

NobitterModel* NobitterControl::model()
{
	return pModel;
}

void NobitterControl::slotReportTwitterError(QHttp::Error error)
{
	QString errorMessage = QString("Twitter API error (%1)").arg(error);
	slotEmitSignalRequestDone();
	emit signalStatusUpdated(errorMessage);
}

void NobitterControl::slotReportTinyUrlError(QHttp::Error error)
{
	QString errorMessage = QString("TinyURL API error (%1)").arg(error);
	emit signalStatusUpdated(errorMessage);
}

void NobitterControl::slotSetUserName(const QString& newName)
{
	if (userName != newName) {
		//! user name has been changed
		if (!userName.isEmpty() && UserSettings::get()->saveLog()) {
			slotSave();
		}
		removeFriend(userName);
		pModel->init();
		userName = newName;
		if (UserSettings::get()->saveLog()) {
			slotLoad();
		}
		authenticationDone = false;
		isModelInitialized = false;
	}
}

void NobitterControl::slotAlertAuthenticationFailure(int requestId)
{
	Q_UNUSED(requestId);
	QMessageBox::critical(QApplication::activeWindow(), "ERROR", "Authentication Failure");
	slotShowSettingsDialog();
}

void NobitterControl::slotSetGroupGatewayName(const QString& newName)
{
	if (groupName != newName) {
		groupName = newName;
		isModelInitialized = false;
		if (authenticationDone) {
			pTwitter->getFollowingUsersOf(groupName);
		}
	}
}

void NobitterControl::slotSendMyTweet(const QString& myTweet)
{
	QString textToSend;
	if (!groupName.isEmpty() && !myTweet.startsWith("@")) {
		textToSend = "@" + groupName + " " + myTweet;
	} else if (myTweet.startsWith("@"+userName+" ")) {
		//! chop off @<my-user-name>s
		textToSend = myTweet.mid(2 + userName.length());
	} else if (myTweet.startsWith("@ ")) {
		//! chop off "@ "
		textToSend = myTweet.mid(2);
	} else {
		textToSend = myTweet;
	}

	pModel->feedMyTweet(Tweet(QDateTime::currentDateTime(), userName, textToSend));

	QString atGroupHeader = "@"+groupName+" ";
	if (!groupName.isEmpty() && textToSend.startsWith(atGroupHeader)) {
		pCipher->encrypt(textToSend.mid(atGroupHeader.size()));
	} else {
		pTwitter->update(textToSend);
	}
}

void NobitterControl::slotSendEncryptedTweet(const QString& text, const QString& encryptedUrl)
{
	Q_UNUSED(text);
	QString atGroupHeader = "@"+groupName+" ";
	QString textToSend = atGroupHeader + NOBITTER_HEADER + encryptedUrl;
	pTwitter->update(textToSend);
}

void NobitterControl::slotFeedDecryptedTweet(const QString& encryptedUrl, const QString& text)
{
	std::map<QString, Tweet>::iterator it = tinyUrlToTweetCache.find(encryptedUrl);
	if (it != tinyUrlToTweetCache.end()) {
		QString atGroupHeader = "@"+groupName+" ";
		QString statusText = atGroupHeader + text;
		Tweet tweet = it->second;
		tweet.setStatusText(statusText);
		TweetVector tweets;
		tweets.push_back(tweet);
		pModel->feedTweets(tweets);
		tinyUrlToTweetCache.erase(it);
	}
}

void NobitterControl::slotShowSettingsDialog()
{
	QString orgPassword = UserSettings::get()->twitterPassword();

	SettingsDialog dialog(QApplication::activeWindow());
	dialog.exec();

	updateProxy();

	//! update group encryption settings
	slotSetGroupGatewayName(UserSettings::get()->gatewayUserName());
	pCipher->setKey(UserSettings::get()->encryptionKey());

	//! update twitter credentials, if changed by the user
	if (userName != UserSettings::get()->twitterUserName()) {
		slotSetUserName(UserSettings::get()->twitterUserName());
		pTwitter->setTwitterCredentials(userName, UserSettings::get()->twitterPassword());
	} else if (orgPassword != UserSettings::get()->twitterPassword()) {
		pTwitter->setTwitterCredentials(userName, UserSettings::get()->twitterPassword());
	}
}

void NobitterControl::slotUpdateMyTimeline()
{
	if (userName.isEmpty() || UserSettings::get()->twitterPassword().isEmpty()) {
		slotShowSettingsDialog();
	} else {
		if (UserSettings::get()->saveLog()) {
			saveLastMonthLog();
		}
		mostRecentDateTime = pModel->mostRecentDateTime();
		pTwitter->getMyfriendsTimeline();
	}
}

void NobitterControl::slotSetGroupMemberList(int requestId, const QStringList& memberList)
{
	Q_UNUSED(requestId);

	groupMemberList = memberList;

	if (groupMemberList.isEmpty()) {
		QString errMsg = QString("Gateway has no following users: %1").arg(groupName);
		QMessageBox::warning(QApplication::activeWindow(), "WARNING", errMsg);
	}

	getGroupMembersTimeline();
}

void NobitterControl::slotFeedTweets(int requestId, const TweetVector& tweets)
{
	Q_UNUSED(requestId);

	//! reach here if and only Twitter authentication succeeds
	authenticationDone = true;

	if (tweets.empty()) {
		return;
	}
	
	if (!isModelInitialized) {
		if (groupMemberList.isEmpty()) {
			QDateTime oldestDateTime = tweets[0].timeStamp();
			for (unsigned int i = 1; i < tweets.size(); ++i) {
				oldestDateTime = qMin(oldestDateTime, tweets[i].timeStamp());
			}
			if (!groupName.isEmpty() && mostRecentDateTime < oldestDateTime) {
				//! need to fill the gap between the most recent msg in the model  
				pTwitter->getFollowingUsersOf(groupName);
			} else {
				isModelInitialized = true;
			}
		} else {
			getGroupMembersTimeline();
		}
	}

	if (groupName.isEmpty()) {
		//! user is not using group message encryption feature
		for (unsigned int i = 0; i < tweets.size(); ++i) {
			addFriend(tweets[i].userName());
		}
		pModel->feedTweets(tweets);
	} else {
		QString atGroupHeader = "@"+groupName+" ";
		QString encryptedMsgHeader = atGroupHeader + NOBITTER_HEADER;
		
		TweetVector plainTweets;
		for (unsigned int i = 0; i < tweets.size(); ++i) {
			Tweet tweet = tweets[i];
			addFriend(tweet.userName());
			if (tweet.statusText().startsWith(encryptedMsgHeader)) {
				QString tinyUrl = tweet.statusText().mid(encryptedMsgHeader.size());
				tinyUrlToTweetCache[tinyUrl] = tweet;
				pCipher->decrypt(tinyUrl);
			} else {
				plainTweets.push_back(tweet);
			}
		}
		pModel->feedTweets(plainTweets);
	}
}


void NobitterControl::getGroupMembersTimeline()
{
	if (!groupMemberList.isEmpty()) {
		//! currently getting the most recent page of each group members
		pTwitter->getTimelineOf(groupMemberList.back());
		groupMemberList.pop_back();
	}

	isModelInitialized = (groupMemberList.isEmpty());
}


void NobitterControl::slotEmitSignalRequestStarted()
{
	emit signalRequestStarted();
}

void NobitterControl::slotEmitSignalRequestDone()
{
	emit signalRequestDone();
}

void NobitterControl::addFriend(const QString& newFriend)
{
	if (myFriends.find(newFriend) == myFriends.end()) {
		myFriends.insert(newFriend);
		updateWhoFilter();
	}
}

void NobitterControl::removeFriend(const QString& oldFriend)
{
	std::set<QString>::iterator it = myFriends.find(oldFriend);
	if (it != myFriends.end()) {
		myFriends.erase(it);
		updateWhoFilter();
	}
}

void NobitterControl::updateWhoFilter()
{
	std::vector<FilterPtr> tempFilters;

	tempFilters.push_back(FilterPtr(new ShowAllFilter(tr("any body"))));
	std::set<QString>::iterator it;
	for (it = myFriends.begin(); it != myFriends.end(); ++it) {
		tempFilters.push_back(FilterPtr(new UserFilter(*it, *it)));
	}

	whoFilters.swap(tempFilters);

	pModel->setWhoFilter(whoFilters[0].get());


	tempFilters.clear();
	
	tempFilters.push_back(FilterPtr(new ShowAllFilter("@<anywhere>")));
	tempFilters.push_back(FilterPtr(new AtUserFilter("@"+userName, userName)));
	if (!groupName.isEmpty()) {
		tempFilters.push_back(FilterPtr(new AtUserFilter("@"+groupName, groupName)));
	}
	
	for (it = myFriends.begin(); it != myFriends.end(); ++it) {
		if (*it != userName) {
			tempFilters.push_back(FilterPtr(new AtUserFilter("@"+*it, *it)));
		}
	}
	
	atFilters.swap(tempFilters);
	
	pModel->setAtFilter(atFilters[0].get());
}

QStringList NobitterControl::getWhoFilterLabels() const
{
	QStringList filterLabels;
	for (unsigned int i = 0; i < whoFilters.size(); ++i) {
		filterLabels.push_back(whoFilters[i]->name());
	}
	return filterLabels;
}

QStringList NobitterControl::getAtFilterLabels() const
{
	QStringList filterLabels;
	for (unsigned int i = 0; i < atFilters.size(); ++i) {
		filterLabels.push_back(atFilters[i]->name());
	}
	return filterLabels;
}

QStringList NobitterControl::getWhenFilterLabels() const
{
	QStringList filterLabels;
	for (unsigned int i = 0; i < whenFilters.size(); ++i) {
		filterLabels.push_back(whenFilters[i]->name());
	}
	return filterLabels;
}

void NobitterControl::slotChangeWhoFilter(int index)
{
	if (0 <= index && index < static_cast<int>(whoFilters.size())) {
		pModel->setWhoFilter(whoFilters[index].get());
	}
}

void NobitterControl::slotChangeAtFilter(int index)
{
	if (0 <= index && index < static_cast<int>(atFilters.size())) {
		pModel->setAtFilter(atFilters[index].get());
	}
}

void NobitterControl::slotChangeWhenFilter(int index)
{
	if (0 <= index && index < static_cast<int>(whenFilters.size())) {
		pModel->setWhenFilter(whenFilters[index].get());
	}
}

void NobitterControl::setupWhenFilter()
{
	whenFilters.push_back(FilterPtr(new ShowAllFilter(tr("any time"))));
	whenFilters.push_back(FilterPtr(new WhenFilter(tr("in the last 7 days"), 7 * 24 * 60 * 60)));
	whenFilters.push_back(FilterPtr(new WhenFilter(tr("in the last 24 hours"), 24 * 60 * 60)));
	whenFilters.push_back(FilterPtr(new WhenFilter(tr("in the last 4 hours"), 4 * 60 * 60)));
	whenFilters.push_back(FilterPtr(new WhenFilter(tr("in the last 2 hours"), 4 * 60 * 60)));
	whenFilters.push_back(FilterPtr(new LatestUpdateFilter(tr("latest only"))));
}

void NobitterControl::slotShowNoSuchGroupError(int requestId)
{
	Q_UNUSED(requestId);

	QString errMsg = QString("Gateway user not found: %1").arg(groupName);
	QMessageBox::warning(QApplication::activeWindow(), "ERROR", errMsg);
}

QStringList NobitterControl::getFriendNames() const
{
	QStringList ret;
	std::set<QString>::const_iterator it;
	for (it = myFriends.begin(); it != myFriends.end(); ++it) {
		ret << ('@' + *it);
	}
	return ret;
}

void NobitterControl::updateProxy()
{
	if (UserSettings::get()->useHttpProxy()) {
		proxy.setType(QNetworkProxy::HttpProxy);		
		proxy.setHostName(UserSettings::get()->httpProxy());
		proxy.setPort(UserSettings::get()->httpProxyPort());
		proxy.setUser(UserSettings::get()->proxyUserName());
		proxy.setPassword(UserSettings::get()->proxyPassword());
	} else {
		proxy.setType(QNetworkProxy::NoProxy);		
	}

	QNetworkProxy::setApplicationProxy(proxy); 
}

bool NobitterControl::slotLoad()
{
	QFile logFile(logFilePath(userName));
	if (!logFile.exists()) {
		return true;
	}

	if (!logFile.open(QIODevice::ReadOnly)) {
		qDebug("Failed to open the log file in Read mode");
		return false;
	}
	
	QTextStream in(&logFile);
	bool succeeded = pModel->load(in);
	logFile.close();

	std::set<QString> userNames = pModel->getUserNames();
	for (std::set<QString>::iterator it = userNames.begin(); it != userNames.end(); ++it) {
		myFriends.insert(*it);
	}

	updateWhoFilter();

	return succeeded;
}

bool NobitterControl::slotSave()
{
	QFile tmpFile(logFilePath(userName) + ".tmp");

	QFileInfo fileInfo(tmpFile);
	QDir logDir = fileInfo.dir();
	if (!logDir.exists()) {
		if (!logDir.mkpath(logDir.absolutePath())) {
			return false;
		}
	}

	if (!tmpFile.open(QIODevice::WriteOnly)) {
		qDebug("Failed to open a tmp file in Write mode");
		return false;
	}

	QTextStream out(&tmpFile);
	bool succeeded = pModel->save(out);
	out.flush();
	tmpFile.close();

	if (succeeded) {
		if (QFile::exists(logFilePath(userName)) && !QFile::remove(logFilePath(userName))) {
			qDebug("Failed to swap the log file with the tmp file");
			succeeded = false;
			tmpFile.remove();
		} else {
			tmpFile.rename(logFilePath(userName));
		}
	}

	return succeeded;
}

void NobitterControl::saveLastMonthLog()
{
	int month = QDate::currentDate().month() - 1;
	int year = QDate::currentDate().year();
	if (month == 0) {
		month = 12;
		--year;
	}

	QString nameAndMonth = userName + QString("-%1-%2").arg(year).arg(month, 2, 10, QChar('0'));

	QFile logFile(logFilePath(nameAndMonth));
	if (!logFile.exists()) {
		QFileInfo fileInfo(logFile);
		
		QDir logDir = fileInfo.dir();
		if (!logDir.exists()) {
			if (!logDir.mkpath(logDir.absolutePath())) {
				return;
			}
		}
		
		if (!logFile.open(QIODevice::WriteOnly)) {
			qDebug("Failed to open a log file in Write mode");
			return;
		}

		pModel->removeTweetsBefore(QDate(year, month, 1));
	
		QTextStream out(&logFile);
		pModel->saveLogInMonth(year, month, out);
		out.flush();
		logFile.close();

		if (logFile.size() == 0) {
			logFile.remove();
		}
	}
}

void NobitterControl::slotQuit()
{
	pTwitter->reset();
	pCipher->registerUserData();
	ImageCache::getInstance()->reset();

	if (UserSettings::get()->saveLog()) {
		slotSave();
	}

	QCoreApplication::instance()->quit();
}

